ゲームプログラムの構造・敵弾処理
▼ 始めに ここではこれまで解説した要素を組み合わせて実際に動くルーチンを作る事に よりゲームプログラムの構造を探っていきます。題材は敵弾処理ルーチンです。 ▼ その1・表示するだけ  先ほどお見せした敵弾表示プログラムです。 ● サンプル実行(要ジョイスティック)=非対応メニューです ◎ ソースリスト このプログラムでは表示しただけで終了してしまうので、これを動くようにし てみます。 ▼ その2・移動するようにしてみる 敵弾を表示できるようになったら次は移動です。では、「移動する」というの はどういう概念なのでしょうか? パソコンは基本的には静止画しか扱えません。私達が日常「動画」と読んでい る物は実際には静止画の連続です。静止画を1秒間に何枚も表示する事により、 人間にこれを動画として認識させることが可能なのです。  これを敵弾処理に当てはめて考えると、 ・座標を少しづつずらして表示する  事になります。スプライトの表示ルーチンを見てみると、 ---- eshot/eshot.c ---- /* 座標(128,144), スプライト No.3, パレット3, 優先順位 $3f */ xsp_set (128, 144, 3, 0x033f); ----------------------- という部分があります。この例では表示座標は (128,144) に固定ですが、こ れを可変にしてみます。座標を変数 eshot_x, eshot_y にして各4ドットづつ移 動するようにしてみます。とりあえず弾は単発で考えると以下のようになります。 ---- eshot_x = 32; /* 初期値 */ eshot_y = 32; /* 初期値 */ /* 無限ループ */ while(1){ xsp_vsync(0); /* 垂直同期待ち */ eshot_x += 4; /* 4ドット右に移動 */ eshot_y += 4; /* 4ドット下に移動 */ /* 座標(eshot_x,eshot_y), スプライト No.1, パレット3, 優先順位 $3f */ xsp_set (eshot_x, eshot_y, 1, 0x033f); } ---- このままだと1回弾が出て終わりですから「ボタンを押したら弾が出る」よう に改良してみます。敵弾がボタンで出るというのもちょっと変な話ですが…。こ れは以下のようなプログラムになります。 ---- if (ボタンが押された) { if (現在弾が出ていない){ eshot_x = 32; /* 初期値 */ eshot_y = 32; /* 初期値 */ } } ---- 「現在弾がでていない」事を確認しています。先ほど弾は単発と仮定したので 「弾が出ている/出ていない」の2種類しか状態がありません。そこでフラグ eshot_use を導入し、これが非 0 なら弾が出ていると考えます。 eshot_use が非 0 のままでは1回弾を出したらそれっきりです。次に弾を出 せるようになるのは弾が画面外に消えた、弾が敵に当たって消えた、の2つの要 因が考えられます。敵に当たった処理は後回しにするとして、まずは画面外に消 えたら eshot_use = 0 にします。 ---- if (弾が画面外に消えた) eshot_use = 0; ---- 弾が画面外に消えたかどうかは座標を調べれば判ります。今回は右下に弾が出 るので eshot_x が 256 を越えたか、 eshot_y が 256 を越えたかを調べれば十 分でしょう。以上をプログラムするとこうなります。 ● サンプル実行(要ジョイスティック)=非対応メニューです ◎ ソースリスト ここまではダラダラと main() 関数1個で処理を行ってきましたが、そろそろ 各機能を関数に切り分ける事を考えてみます。  敵弾の処理を機能単位で考えると、 ・敵弾の発生 ・敵弾の移動 ・敵弾の消去  の3つに分かれます。現実的には、 ・ゲーム開始時の初期化  が必要となるので関数を4つに分けてみます。 ・ゲーム開始時の初期化 : EshotInit() ・敵弾の発生 : EshotAlloc() ・敵弾の移動 : EshotMove() ・敵弾の消去 : EshotFree()  これを念頭に置いた上で次項へ。 ▼ その3・複数の弾を発生させてみる 複数の弾を撃つには敵弾関係のワークを弾数だけ用意します。ここでは簡単に 配列を使ってみます。 ---- eshot3/eshot.c ---- #define ESHOT_MAX 200 /* 敵弾最大数 */ signed short eshot_x[ESHOT_MAX]; /* 敵弾のX座標 */ signed short eshot_y[ESHOT_MAX]; /*  〃 Y座標 */ unsigned char eshot_type[ESHOT_MAX]; /*  〃 種類(=0 なら未使用) */ ------------------------ 大盤振舞で 200発まで発生可能にします。これを配列で持たせます。先ほど eshot_use だった弾が出ているか/いないかを表すフラグは、少し拡張して弾の 種類 eshot_type[] にします。これが 0 ならそのワークは未使用、n(非 0)な らば n という種類の弾が出ていると考えます。n が 1 なら通常弾、2 ならレー ザー、3 なら誘導弾…という使い方を想定しています。ワークの使い方について は引き数を用いて、 eshot_x[0], eshot_y[0], eshot_type[0] : 弾 0 の座標、種類 eshot_x[1], eshot_y[1], eshot_type[1] : 弾 1 の座標、種類 : eshot_x[199], eshot_y[199], eshot_type[199] : 弾 199 の座標、種類  とします。  さて、このワークを確保したらまずは初期化します。 ---- eshot3/eshot.c EshotInit() ---- for (i = 0; i < ESHOT_MAX; i++) eshot_type[i] = 0; /* 全部未使用に */ ------------------------------------ 次に敵弾の発生ですが、これは先ほどのルーチンを配列に対応させるだけです。 前回は単発だったため、弾が出ているかどうかを調べるのに eshot_use だけで 済みましたが、今回は複数の弾が出るようにするので、「そのワークが使われて いるか?(使われていたら既に弾が出ている)」ということで eshot_type[] を 調べます。具体的には eshot_type[0]〜eshot_type[199] までを調べていって、 未使用(eshot_type[] == 0) であればそのワークを使用します。 ---- eshot3/eshot.c EshotAlloc() ---- for (i = 0; i < ESHOT_MAX; i++) { /* 未使用のワークを見つける */ if (eshot_type[i] == 0) { /* 未使用のワークを見つけた */ eshot_x[i] = x; eshot_y[i] = y; eshot_type[i] = type; break; /* for ループを抜ける */ } } ------------------------------------- 次に移動ルーチンです。移動ルーチンではワークを調べていって使用中ならス プライトを移動して表示します。もし画面外に出るようであれば eshot_type[] = 0 (未使用)にします。 ---- eshot3/eshot.c EshotMove() ---- for (i = 0; i < ESHOT_MAX; i++) { /* そのワークは使用中か? */ if (eshot_type[i] != 0) { /* 使用中のワークを見つけた */ eshot_x[i] += 4; /* 4ドット右に移動 */ eshot_y[i] += 4; /* 4ドット下に移動 */ /* 画面外にでたら敵弾を消去(未使用)に */ if ((eshot_x[i] > 256) || (eshot_y[i] > 256)) { EshotFree (i); } else { /* 座標(eshot_x,eshot_y), スプライト No.1, パレット3, 優先順位 $3f */ xsp_set (eshot_x[i], eshot_y[i], 1, 0x033f); } } } ------------------------------------  消去ルーチンは指定されたワークの eshot_type[] を 0 にします。 ---- eshot3/eshot.c EshotFree() ---- eshot_type[i] = 0; /* 未使用に */ ------------------------------------  以上をプログラムすると以下のようになります。 ● サンプル実行(要ジョイスティック)=非対応メニューです ◎ ソースリスト ▼ その4・固定小数点を使ってみる 今までは座標を signed short で扱ってきましたが、ここで固定小数点を使っ てみます。signed int lx,ly が固定小数点での座標で、整数16bit + 小数16bit で表します。また、速度も signed int vx,vy という固定小数値で表します。 敵弾に関係する数値が増えてきてちょっとごちゃごちゃしてきました。そこで これらを全て ESHOT 構造体に入れてしまいます。 ---- eshot4/eshot.c ---- /* 敵弾構造体 */ typedef struct { signed short x; /* 敵弾のX座標 */ signed short y; /*  〃 Y座標 */ unsigned char type; /*  〃 種類(=0 なら未使用) */ signed int lx, ly; /* 32bit X,Y 座標 ( l = longword ) */ signed int vx, vy; /* 32bit X,Y 速度 ( v = velocity ) */ } ESHOT; ESHOT eshot[ESHOT_MAX]; /* 敵弾のワーク */ -------------------------  これで敵弾は eshot[i].x のような形で扱えるようになりました。 さて、ここで固定小数点ということで敵弾の速度を 0.75ドット/フレームと してみます。整数値で表すには面倒な微妙な速度も固定小数点なら簡単です。こ の 0.75ドットというのは固定小数値では 65536*0.75 = 49152 となります。  更に sin,cos テーブルを使って 256方向へと弾が撃てるようにします。 ---- eshot4/eshot.c ---- for (i = 0; i < 256; i++) { xytable[i].x = (signed int) (cos (2.0 * M_PI * (long) i / 256.0) * 65536.0 * ESHOT_SPEED); xytable[i].y = (signed int) (sin (2.0 * M_PI * (long) i / 256.0) * 65536.0 * ESHOT_SPEED); } ------------------------ 座標の計算は一旦固定小数点で計算し、それから整数部を取り出して従来通り の signed short へと変換します。 ---- eshot4/eshot.c EshotMove() ---- eshot[i].lx += eshot[i].vx; /* vx だけ移動 */ eshot[i].ly += eshot[i].vy; /* vy だけ移動 */ eshot[i].x = eshot[i].lx / 65536; /* lx の整数部を取り出して x に */ eshot[i].y = eshot[i].ly / 65536; /* ly の整数部を取り出して y に */ ------------------------------------ そうそう、敵弾が画面外へでた判定をちゃんと画面の上下左右から出たかどう か、4種類の判定を行うようにしました。今までは右下からでると決まっていた ので手を抜いていましたが、256方向へでるようにした以上、ちゃんと行う必要 があります。 ---- eshot4/eshot.c EshotMove() ---- /* 画面外にでたら敵弾を消去(未使用)に */ if ((eshot[i].x < 0) || (eshot[i].x > 256) || (eshot[i].y < 0) || (eshot[i].y > 256)) { ------------------------------------ 直接は関係ありませんが、画面のまんなかに線を引くことにしました。座標は (0,128)-(255,128) (128,0)-(128,255) です。  ではサンプルをどうぞ。 ● サンプル実行(要ジョイスティック)=非対応メニューです ◎ ソースリスト あれれ、画面中央から弾が出るはずなのに、出ていませんね。これには理由が あります。以下を御覧下さい。 ◎ 自機と敵弾のスプライト座標 xsp_set() で表示する単体のスプライトはその左上の座標を指定します。です からもし画面中央から出したいのであれば、座標を (-8,-8) する必要がありま す。 余談ですが、X680x0 のスプライト画面は他の画面に比べて座標が (-16,-16) ドットずれています。画面左上が (16,16) です。これは画面端のスプライトの 処理を扱い易くするためにこういう仕様になっているのだと思うのですが…。 ▼ その5・ワークをリスト構造で管理してみる 前項までの処理が理解できればほぼゲームは作成できます。おそらくここまで の技術のみを用いて作られたゲームも沢山ある事でしょう。 が、しかし、X680x0 で最強のシューティングゲームを目指す我々としてはこ れだけで満足するわけにはいきません。更なるパフォーマンス向上策について考 察していきましょう。 eshot4.x は画面上にある一定数以上の弾を撃つと処理落ちを起こします。ど れくらいの弾数で処理落ちを起こすのかはMPUパワーによって異なりますが、 10NHz 機では80発程度で処理落ちが起こります。処理落ちが起きたかどうかは 画面上の弾の移動速度が遅くなることで確認できます。このような処理落ち現象 を見逃さない目もプログラマーには必要です(速いマシンを使用している方はク ロックを落とす、キャッシュをオフにする等して確認して下さい)。  では eshot4.x のどこが遅いのか。ソースリストを眺めつつ検討してみます。  まず EshotAlloc()。この中にまずい部分があります。 ---- eshot4/eshot.c ---- for (i = 0; i < ESHOT_MAX; i++) { /* 未使用のワークを見つける */ if (eshot[i].type == 0) { ------------------------ 弾発生時に未使用のワークを見つける部分です。ワークの0番から199番まで 調べていくのですが、弾を発生する度に(最悪)全部のワークを調べなければな りません。また、この部分は弾数が少ない(ワークの空きが多い)時は高速に動 作しますが、弾数が多い(ワークの空きが少ない)時ほど低速になる事にも注意 しなければなりません。「処理落ちの理論」でも述べた通り、これは好ましくあ りませんね。  もう一つまずい点、それは実は「配列自体が遅い」という点です。 配列と言うのは何でしょう? この問いを本当に理解するためにはアセンブラ の知識が必要になります。gcc -S でアセンブラのソースリストを出力しますが、 ここでは gcc の限界を見極めるため最適化オプションを山ほど付けてみましょ う。  この結果が顕著に現れるのが EshotFree() です。 ---- eshot4/eshot.c ---- /* 敵弾消去時に呼ばれる */ void EshotFree (unsigned short i) { eshot[i].type = 0; /* 未使用に */ } ------------------------ なんのことはない、配列の中の1要素をクリアするだけのルーチンですが、こ れをコンパイルすると以下のようになります。 gcc -S -O -fomit-frame-pointer -fstrength-reduce -fforce-mem -fforce-addr -fcombine-regs eshot.c ---- eshot4/eshot.s ---- _EshotFree: moveq.l #0,d0 move.w 6(sp),d0 * d0 = 引き数 i move.l d0,d1 * asl.l #2,d1 * d0.l を 22倍している add.l d0,d1 * 22 というのは sizeof(ESHOT) です add.l d1,d1 * add.l d0,d1 * add.l d1,d1 * d1.l = d0.l * 22 move.l d1,a0 add.l #_eshot+4,a0 * +4 は .type のオフセット値 clr.b (a0) * 0 を代入 rts ------------------------  これをC言語で書くと以下のようになります。 ---- *(eshot + i * sizeof(ESHOT) + 4) = 0; ---- 御覧の通り、配列というのは内部では乗算を使用している衝撃の事実が明らか になりました。乗算は M68000 では遅いことで有名な命令で、とにかく乗算を廃 する事が高速化の初歩と言っても過言ではありません。もっとも、 gcc は賢い ので乗算と言っても mulu.w 等は使わずに高速な加算とシフトに展開しています が、それでも速いとは言い難いコードです。  ではどうするか。対応策は以下の通りです。 1)乗算の速いMPUを使用する 根本的な解決策ではありますが、人事を尽くしていないのに天命を待つのはど うかと思います。 2)乗算を加算とシフトに展開する  少しだけ高速になります。gcc はコレ。 3)乗算が速くなるように細工する 上記で 22 を掛けているのは sizeof(ESHOT) が 22 だからですが、これを 2^n にすると乗算がシフトだけになり、高速化されます。このために ESHOT 構造体に 10バイトの詰め物をして32バイトにしてみます。 ---- /* 敵弾構造体 */ typedef struct { signed short x; /* 敵弾のX座標 */ signed short y; /*  〃 Y座標 */ unsigned char type; /*  〃 種類(=0 なら未使用) */ signed int lx, ly; /* 32bit X,Y 座標 ( l = longword ) */ signed int vx, vy; /* 32bit X,Y 速度 ( v = velocity ) */ char dummy[10]; /* 詰め物 */ } ESHOT; ----  結果は以下のようになります。 ---- eshot4/eshot.s ---- _EshotFree: moveq.l #0,d0 move.w 6(sp),d0 asl.l #5,d0 * 32倍 move.l d0,a0 add.l #_eshot+4,a0 clr.b (a0) rts ------------------------ 乗算がシフトだけになり、高速化されました。めでたしめでたし、と言いたい 所ですが、実は、M68000 は、シフトも遅いのです。乗算よりは速いのですが…。  ではどうするか。おそらく一番正しいであろう答は、 4)配列を使わない ことです。では、どのようなデータ構造を使えば良いのでしょうか? 予定調 和な回答ではありますが、それは「リスト構造」です。リスト構造についてはC 言語またはアルゴリズムの教科書を御覧下さい…ではあまりに冷たいので軽く説 明します。  リスト構造は各ワーク(ノードと言う)をポインタでつないだデータ形式です。 データ本体 ポインタ ┌────┬─┐ ┌────┬─┐ ┌────┬─┐ │ノードA│──→│ノードB│──→│ノードC│0│ └────┴─┘ └────┴─┘ └────┴─┘ 先頭のノードは次のノードをポインタで指し、次のノードは更に次のノードを ポインタで指し…と続き、最後のノードは「もう次がない」事を示すために「次 のノード」として目印を入れておく(図では0ですが、C言語では NULL です) というものです。リスト構造の利点であるノードの高速な挿入/削除は今回のよ うな用途にはうってつけです。それではリスト構造使用に書き換えてみます。 ● サンプル実行(要ジョイスティック)=非対応メニューです ◎ ソースリスト  では今回の改造点の見所を。 ---- eshot5/eshot.c ---- /* 敵弾構造体 */ typedef struct _eshot {  : (中略) struct _eshot *next; /* 次の構造体へのポインタ */ } ESHOT; ------------------------  リスト構造を作るために *next というメンバーを追加しています。 ---- eshot5/eshot.c ---- /* 引き数 : 種類, X座標, Y座標, 角度 */ void EshotAlloc (unsigned char type, signed short x, signed short y, unsigned char angle) { ESHOT *p; if (eshot_count >= ESHOT_MAX) /* 敵弾数が ESHOT_MAX を越えているか? */ return; /* これ以上は出さない */ eshot_count++; p = malloc (sizeof (ESHOT)); /* ノードを1個確保 */ if (p == NULL) /* 確保できたか? */ return; /* 確保できなかった(メモリ不足) */ /* リスト構造で管理 */ if (eshot_top == NULL) { /* 1個も弾が出ていない場合 */ p->next = NULL; /* 次はない */ } else { p->next = eshot_top; /* 「今までの先頭」を「今確保したノード」の次にする */ } eshot_top = p; /* 今確保したノードを先頭にする */ ------------------------ malloc() でノード(ワーク)を1つ確保してリストにつなげています。先ほ どは未使用ワークを捜すために for() でループしていましたが、リスト構造を 使えばリストをつなげるだけで、ワーク使用数に関わらず常に一定の速度でワー クの確保が可能です。 ---- eshot5/eshot.c ---- /* 垂直同期ごとに呼ばれる */ void EshotMove (void) { ESHOT *p, *q; p = eshot_top; /* 現在注目しているワーク */ q = NULL; /* 1つ前のワーク(ワーク削除時に必要) */ while (p != NULL) { : (中略) if (q == NULL) { /* リストの一番最初を削除 */ eshot_top = p->next; free (p); /* メモリを開放 */ q = NULL; p = eshot_top; } else { q->next = p->next; free (p); /* メモリを開放 */ p = q->next; } ------------------------ 今まで各ワークを調べるのに for() ループを用いていましたが、リスト構造 の場合「リストの最後まで」ということで while() ループに変更してあります。 ワークを開放するには今までの .type に0を書き込む代わりに free() でワー クそのものを開放してしまいます。 リスト構造とは関係ありませんが、前項の「スプライトがずれるバグ」にも対 処しておきました。 ▼ その6・未使用ワークもリスト構造で管理してみる これで大分高速化できました。が、しかしこのソースにはまだまだ改良の余地 があります。遅い部分はずばり、malloc() / free() を使っているところです。 この2つの関数は libc の関数でメモリの確保/開放を行う関数です。実測して いないのでなんとも言えないのですが、これらの関数は比較的遅い関数です。な ぜ遅いか、という事はメモリ管理の内部実装という深い問題になるので割愛しま すが、もう少し速い方法が望まれます。巷では GNU MALLOC という高速な malloc() / free() 関数が出回っていますのでそちらを使うというのも一つの手 ですが、根本的な解決法として「ゲーム中に malloc() / free() を使わない」 というのがあります。 malloc() / free() を使わずにどうやってリスト構造を実現するのでしょう? 実は「ゲーム中に」というのがポイントで、以下のような方法を使います。 1)起動時にまとめて全ワークを確保する 2)それを全てリストでつなげて「未使用ワークのリスト」を作る 3)ワーク使用時は「未使用ワークのリスト」からワークを持ってくる 4)ワークが不要になったら「未使用ワークのリスト」につなげる つまり未使用ワークもリストにしてしまうのです。図示すると以下のようになり ます。 1)初期状態 使用中ワークのリスト   (なし) 未使用ワークのリスト ┌────┬─┐ ┌────┬─┐ ┌────┬─┐ │ノードA│──→│ノードB│──→│ノードC│0│ └────┴─┘ └────┴─┘ └────┴─┘ 2)ワークを1個確保 使用中ワークのリスト ┌────┬─┐ │ノードA│0│ └────┴─┘ 未使用ワークのリスト ┌────┬─┐ ┌────┬─┐ │ノードB│──→│ノードC│0│ └────┴─┘ └────┴─┘ 3)ワークを2個確保 使用中ワークのリスト ┌────┬─┐ ┌────┬─┐ │ノードA│──→│ノードB│0│ └────┴─┘ └────┴─┘ 未使用ワークのリスト ┌────┬─┐ │ノードC│0│ └────┴─┘ 4)ワークを1個開放 使用中ワークのリスト ┌────┬─┐ │ノードA│0│ └────┴─┘ 未使用ワークのリスト ┌────┬─┐ ┌────┬─┐ │ノードB│──→│ノードC│0│ └────┴─┘ └────┴─┘ つまりリストを2本用意して、ノードを付けたり外したりするだけです。これ だけならゲーム中はポインタの操作だけですので malloc() / free() は必要あ りません。その代わり起動時に未使用ワークのリストを作る処理が必要になりま す。 使用中ワークのリストは *eshot_top で、未使用ワークのリストは *eshot_null_top で管理しています。ではサンプルを御覧下さい。 ● サンプル実行(要ジョイスティック)=非対応メニューです ◎ ソースリスト ---- eshot6/eshot.c ---- /* ゲーム開始時に呼ばれる */ void EshotInit (void) { int i; /* リストをつなげる */ eshot_top = NULL; eshot_null_top = eshot; for (i = 0; i < ESHOT_MAX; i++) eshot[i].next = &eshot[i + 1]; /* 次を指すようにする */ eshot[ESHOT_MAX - 1].next = NULL; /* 「一番最後の次」はない */ } ------------------------ 未使用ワークのリストを作るのに malloc() を使わずちょっとテクニカルな方 法を使用してしまいました。eshot[] を配列として確保してしまい、その中をリ ストでつなげるという方法ですが、malloc() を使用するとエラーチェックが必 要となるため、配列として静的に確保してみました。 エラーチェックは安定したプログラムを作るのには欠かせませんが、そうは言っ てもやはり面倒です。それを回避するにはこのように「そもそもエラーが起こら ない」ようなアルゴリズムも有効なのです(配列の確保でエラーは発生しません からね)。もっとも、今回の場合使用ワーク数の上限値が決まっているのでこの ような方法が使えると言うことを忘れずに。 ▼ その7・完璧版(少しやりすぎ) 更に高速化してみます。とりあえずこれが現時点での筆者の技術の限界です。 ・xsp_set_st() を使う XSPのマニュアルを読むと xsp_set() の代わりに座標・スプライトパター ンNo.・反転コード他を入れたワークのポインタを渡す xsp_set_st() という コールがあり、こちらの方が高速と書かれています。こちらを使うために ESHOT 構造体の構造を変更します。 ---- eshot7/eshot.c ---- /* 敵弾構造体 */ typedef struct _eshot { /* 以下の4項目は xsp_set_st() で使うため順不同 */ signed short x; /* 敵弾のX座標 */ signed short y; /*  〃 Y座標 */ short pt; /* スプライトパターンNo. */ short info; /* 反転コード・色・優先度を表わすデータ */ :(略) ------------------------ ・関数の引き数に char や short の値を渡すと遅い 最近まで知らなかったのですが、gcc は関数の引き数として char や short の値を渡すとそれを int に拡張してからスタックに積みます。例によって gcc -S で出力したソースを御覧下さい ---- eshot6/eshot.c ---- EshotAlloc (type, 144, 144, angle); /* 種類 type の敵弾を座標(144,144)、角度 angle で発生 */ ------------------------  これをコンパイルします。 ---- eshot6/eshot.s ---- moveq.l #0,d0 move.b d7,d0 move.l d0,-(sp) pea 144.w pea 144.w moveq.l #0,d0 move.b d6,d0 move.l d0,-(sp) jbsr _EshotAlloc ------------------------ char (.b) の値をわざわざ int (.l) に拡張してから積んでいますね(short の値は今回は即値だったので pea 144.w で積んでいます)。無駄ですね。では どうするか。「一時的な構造体を作ってそのポインタを渡す」のが正解です。そ のため EshotAlloc() の引き数を以下のように変更します。 ---- eshot7/eshot.c ---- /* EshotAlloc の引き数構造体 */ typedef struct { signed short x; /* 敵弾のX座標 */ signed short y; /*  〃 Y座標 */ unsigned char type; /*  〃 種類 */ unsigned char angle; /* 角度 (0〜255) */ } ESHOTA; void EshotAlloc (ESHOTA * a) ------------------------ EshotAlloc() を呼ぶだけのために ESHOTA 構造体というのを新設し、これへ のポインタを渡すことにします。この変更により呼びだし側は以下のように変更 されます。 ---- eshot7/eshot.c ---- { ESHOTA eshota, *_eshota = &eshota; _eshota->x = 144; _eshota->y = 144; _eshota->type = type; _eshota->angle = angle; EshotAlloc (_eshota); } ------------------------  そうするとコンパイル結果は以下のようになります。 ---- eshot7/eshot.s ---- move.w #144,(a3) move.w (a3),2(a3) move.b d6,4(a3) move.b d7,5(a3) move.l a3,-(sp) jbsr _EshotAlloc ------------------------ char を無理矢理 int に拡張することもなくなりましたね。ただ、先ほどまで は EshotAlloc (type, 144, 144, angle); と1行で書けた処理が8行になって しまうのはちょっと冗長ですね。そこでこれをマクロにしてしまいます。 ---- eshot7/eshot.c ---- #define EshotAlloc(eshot_x,eshot_y,eshot_type,eshot_angle) ¥ {¥ ESHOTA eshota, *_eshota = &eshota;¥ _eshota->x = eshot_x;¥ _eshota->y = eshot_y;¥ _eshota->type = eshot_type;¥ _eshota->angle = eshot_angle;¥ EshotAllocF (_eshota);¥ } ------------------------  EshotAlloc() 関数は EshotAllocF() と変更します。 ---- eshot7/eshot.c ---- /* 敵弾出現時に呼ばれる */ /* 引き数 : ESHOTA 構造体 */ void EshotAllocF (ESHOTA * a) ------------------------  そうすると見た目上、従来通りの方法で呼び出せます。 ---- eshot7/eshot.c ---- EshotAlloc (144, 144, type, angle); ------------------------ ・配列の引き数にレジスタを使わせる  以下の何気ないソースを御覧下さい。 ---- eshot6/eshot.c ---- void EshotAlloc (ESHOTA * a) { : p->vx = xytable[a->angle].x; p->vy = xytable[a->angle].y; ------------------------  三角関数テーブルを引いて速度を求めています。これをコンパイルすると、 ---- eshot6/eshot.s ---- moveq.l #0,d0 move.b (a0),d0 * a0.l = a なので d0.l = angle になります lea _xytable,a1 asl.l #3,d0 move.l (a1,d0.l),18(a2) moveq.l #0,d0 move.b (a0),d0 asl.l #3,d0 move.l 4(a1,d0.l),22(a2) move.b 4(a3),d0 move.b d0,8(a2) ------------------------ 2回テーブルを引くのに乗算(シフト)を2回やっています。まあ当たり前と 言えば当たり前なのですが、 ---- eshot6/eshot.s ---- moveq.l #0,d0 move.b 5(a3),d0 * 5(a3) = a->angle asl.l #3,d0 lea _xytable,a0 add.l d0,a0 move.l (a0)+,18(a2) move.l (a0),22(a2) ------------------------  のようにスマートにやってくれても良いものですが。はて。 これを見抜くためには gcc の「変数のレジスタ割り付け」を把握する必要が あります。gcc は、 ・関数の引き数 例)void EshotAlloc (ESHOTA * a) の a など ・ローカル変数 例)int i; など はこれをレジスタに割り当ててくれますが、 ・-> で指された構造体のメンバー 例)a->angle など はレジスタに割り当ててくれません。そのため、頻繁に使う構造体のメンバー は明示的にローカル変数にコピーして使うことにより、レジスタに変数を割り当 ててくれます。 ---- eshot7/eshot.c ---- { int a_angle = a->angle; p->vx = xytable[a_angle].x; p->vy = xytable[a_angle].y; } ------------------------  このように a->angle をローカル変数 a_angle にコピーすることにより、 ---- eshot7/eshot.s ---- moveq.l #0,d0 move.b 5(a3),d0 * 5(a3) = a->angle asl.l #3,d0 lea _xytable,a0 add.l d0,a0 move.l (a0)+,18(a2) move.l (a0),22(a2) ------------------------  a_angle を d0.l に割り当てる事ができます。 なおかつ、gcc は「複雑な式が続くと最適化を諦める」という傾向があります。 これはどういう事かと言うと、 ---- eshot7/eshot.c ---- p->vx = xytable[a->angle].x; p->vy = xytable[a->angle].y; ------------------------  の場合、 ・a->angle をレジスタへ持ってくる ・アドレス _xytable を足す ・メンバー .x のぶんを 18(a2) としてアクセス ・a->angle をレジスタへ持ってくる ・アドレス _xytable を足す ・メンバー .y のぶんを 22(a2) としてアクセス という手順を踏みます。この「a->angle をレジスタへ」というのが元凶で、 これをローカル変数にしてしまうと、 ・アドレス _xytable を足す ・メンバー .x のぶんを 18(a2) としてアクセス ・メンバー .y のぶんを 22(a2) としてアクセス  のように最適化してくれるようです。 ・連続する処理はつなげて書く  以下のコードを御覧下さい。 ---- p->lx += p->vx; /* vx だけ移動 */ p->ly += p->vy; /* vy だけ移動 */ p->x = p->lx / 65536; /* lx の整数部を取り出して x に */ p->y = p->ly / 65536; /* ly の整数部を取り出して y に */ ---- 1行目の演算結果の p->lx は次の行を計算すると消えてなくなってしまいま す。これを以下のように書くだけで高速化されます。 ---- /* 速度を足して上位ワード(固定整数部)だけ取り出す */ p->x = (p->lx += p->vx) >> 16; p->y = (p->ly += p->vy) >> 16; ---- こうする事によりレジスタに残っていた p->lx を次の演算に使い回すので高 速化が可能です。/ 65536 を >> 16 にしたのも高速化に一役買っています…と 言いたいところですが、gcc は除算も(可能ならば)シフトに展開してしまうの で実は意味がありません(でもおまじない)。  この部分、実際には、 ---- eshot7/eshot.c ---- signed short p_x, p_y; /* p->x, p->y と同じ */ /* 速度を足して上位ワード(固定整数部)だけ取り出す */ p_x = p->x = (p->lx += p->vx) >> 16; p_y = p->y = (p->ly += p->vy) >> 16; ------------------------ のように明示的にローカル変数 p_x, p_y に値をコピーしていますが、これは 前述の通りです。ここでは大した速度向上にはなっていませんが、当たり判定を 行う時には、この最適化が極めて有効に働いてきます。 ・画面外へ出た判定 ---- eshot7/eshot.c ---- /* 敵弾が画面外に出たか? */ /* (画面右から出た判定と左から出た判定を1回の比較で行っている) */ if (((unsigned short) p_x > 256 + 16) || ((unsigned short) p_y > 256 + 16)) { ------------------------  これは以下と等価です。 ---- signed short p_x, p_y; /* p->x, p->y と同じ */ : if (( p_x > 256 + 16) || ( p_x < 0 ) || ( p_y > 256 + 16) || ( p_y < 0 )) { ---- signed short の値を unsigned short と見なして「0以下かどうか」の判定 を省略しています。良く判らない人は「符号付き数とは何か」について考えて下 さい。これにより上下左右4回必要だった「敵弾が画面外に出たかどうか」の判 定が2回ですんでいます。弾200発ならば400回分の処理が浮きましたね。  という事で完璧版。 ● サンプル実行(要ジョイスティック)=非対応メニューです ◎ ソースリスト ちょっーとこれは最適化し過ぎたかな?という感はあります。最適化自体は嬉 しいのですが、若干可読性を犠牲にしすぎた嫌いがあります。特に関数の引き数 を変更するためにマクロまで導入したのはやり過ぎかな、と反省しています。そ こで今回のゲームでは今まで説明した最適化手法からその一部を使うだけにとど めたいと思います。とりあえず性能も十分ですしね。でももし、あなたが1画面 200発を超える、1画面256発の超弾幕を必要としているのならば、これらの手法 は十分検討する必要があります。 (EOF)